import logging
from cloudcontrol.exceptions import UserConfigurationMismatchError, UserAlreadyConnectedError, UserConnectionStringInvalidError, NonzeroReturnStatusError
from cloudcontrol._connections import create_connection
from cloudcontrol._io import get_password_input, write_stderr, Expecter
from cloudcontrol._support import get_ip_from_hostname
from cloudcontrol._logging import log_event


_DEFAULT_SSH_PORT = 22
_MAX_USERLIST_LENGTH = 5


class _RunResponse(dict):
    """helper class for wrapping dictionary responses from the 'run' command"""
    
    users = property(lambda self: self.keys())
    
    def __init__(self, other_dict):
        super(_RunResponse, self).__init__(other_dict)


class _ShellCommandRunResponse(_RunResponse):
    """helper class for shell-command-specific RunResponses, which
    only have (stdout,stderr) as their output"""
    
    def _get_stdout(self):
        if len(self.users) == 0:
            raise ValueError("no users participated in this response")
        elif len(self.users) == 1:
            return self.stdout_for(self.users[0])
        else:
            raise ValueError("cannot retrieve simple 'stdout' when multiple connections participated in a response")
    
    def _get_stderr(self):
        if len(self.users) == 0:
            raise ValueError("no users participated in this response")
        elif len(self.users) == 1:
            return self.stderr_for(self.users[0])
        else:
            raise ValueError("cannot retrieve simple 'stdout' when multiple connections participated in a response")
    
    stdout = property(_get_stdout)
    stderr = property(_get_stderr)
    
    def stdout_for(self, user):
        return self[user][0]
        
    def stderr_for(self, user):
        return self[user][1]


class User(object):
    """high-level interface to cloudcontrol via declarative configuration of Users"""
    
    _shared_attr_registry = {}
    """keeps a mapping of all configuration values for a user"""
    
    _password_group_lookup = {}
    """maintains a mapping of password_group --> connection_string"""
    
    _connection_string = property(lambda self: self.__connstring)
    """internal attribute used to identify this user"""
    
    @classmethod
    def _set_password_group(klass, connstring, groupname):
        """internal helper for setting the password group for this user"""
        
        # remove this guy from the default group lookup
        if None not in klass._password_group_lookup:
            klass._password_group_lookup[None] = []
        default_group = klass._password_group_lookup[None]
        if connstring in default_group:
            default_group.remove(connstring)
        
        # add to the new group
        if groupname not in User._password_group_lookup:
            User._password_group_lookup[groupname] = []
        User._password_group_lookup[groupname].append(connstring)
    
    @classmethod
    def _get_connstrings_in_password_group(klass, groupname):
        """returns a list of connection strings that are in the given password_group"""
        if groupname not in klass._password_group_lookup:
            raise StandardError("the password_group '%s' does not exist" % groupname)
        else:
            return klass._password_group_lookup[groupname]
    
    @classmethod
    def _set_shared_attr(klass, connstring, key, value):
        """internal helper for setting an attribute value for a user"""
    
        # ensure that the dict exists
        if connstring not in User._shared_attr_registry:
            User._shared_attr_registry[connstring] = {}
    
        # now the dict exists
        attrs = User._shared_attr_registry[connstring]
        if key in attrs.keys():

            # the attribute has already been set, ensure we
            # aren't conflicting with that value
            if value != attrs[key]:
            
                # conflict found
                raise UserConfigurationMismatchError("cannot re-set '%(key)s' for user '%(connstring)s to %(attempted_val)s - it is already set to '%(current_val)s" % dict(
                    key = key,
                    connstring = connstring, 
                    attempted_val = attempted_val,
                    current_val = attrs[key]
                ))
        
            else:
            
                # no conflicts found
                pass
    
        else:
        
            # the attribute has not been set yet, just set it
            attrs[key] = value

    @classmethod
    def _get_shared_attr(klass, connstring, key):
        """internal helper for getting an attribute value for a user"""

        # ensure that the dict exists
        if connstring in User._shared_attr_registry:
        
            # attr may exist
            if key in User._shared_attr_registry[connstring]:
            
                # attr does exist
                return User._shared_attr_registry[connstring][key]
        
            else:

                # attr doesn't exist
                raise AttributeError("user '%(connstring)s' has no attribute '%(key)s'" % dict(
                    connstring = connstring,
                    key = key,
                ))
    
        else:
        
            # attr lookup doesn't exist
            raise AttributeError("user '%(connstring)s' has no attribute '%(key)s' (no attributes found at all)" % dict(
                connstring = connstring,
                key = key,
            ))
            
    @classmethod
    def _get_shared_attr_with_default(klass, connstring, key, default):
        """internal helper that hides the AttributeError from _get_shared_attr
        and returns the given default value instead"""
        try:
            return klass._get_shared_attr(connstring, key)
        except AttributeError:
            return default
    
    def _create_connection(self):
        """internal helper that sets up the connection for this User"""
        
        if hasattr(self, "su_from"):
            
            # we are su'ing from another user
            su_user = User(self.su_from)
            return create_connection(
                username = self.username,
                hostname = self.hostname,
                port = self.port,
                getpass = self._get_password,
                su_source_username = su_user.username,
                su_source_getpass = su_user._get_password,
            )
        
        else:
            
            # we have a normal connection
            return create_connection(
                username = self.username,
                hostname = self.hostname,
                port = self.port,
                getpass = self._get_password,
            )
    
    def _get_password(self):
        """internal helper that retrieves the password for this User object"""
        log_event(self.__logger.info, "getting_user_password")
        
        if self._password:
            log_event(self.__logger.info, "returning_stored_password")
            
            # our password is already set, just return it
            return self.password
        
        elif self._password_callback:
            log_event(self.__logger.info, "using_existing_password_callback")
            
            # we already have a password callback, call it
            return self._password_callback()
            
        else:
            log_event(self.__logger.info, "getting_group_password")
            
            # our password has not yet been set
            # create a nice password prompt
            connstrings_in_group = User._get_connstrings_in_password_group(self._password_group)
            shortlist = connstrings_in_group[0:_MAX_USERLIST_LENGTH]
            num_not_shown = len(connstrings_in_group) - _MAX_USERLIST_LENGTH
            for connstring in shortlist:
                write_stderr("%s\n" % connstring)
            if num_not_shown > 0:
                write_stderr("...and %s other users\n" % num_not_shown)
            
            # get the password
            password = get_password_input("Password:")
            
            # set that password for all users in this group
            for connstring in connstrings_in_group:
                User._set_shared_attr(connstring, "_password", password)
            
            # return that password
            return password
    
    def _get_username_and_hostname_and_port(self):
        """internal helper that gives the username/hostname based on
        the internal connection string"""
        
        # parse the connection string to set some attributes
        if "@" not in self.__connstring:
        
            # bad connection string
            raise UserConnectionStringInvalidError("cannot parse user connection string '%(connstring)s'" % dict(connstring = self.__connstring))
    
        # otherwise, assume that the first token is username
        # and the rest is hostname
        username, hostname = self.__connstring.split("@", 1)
        if ":" in hostname:
            hostname, port = hostname.split(":", 1)
        else:
            port = _DEFAULT_SSH_PORT
        return (username, hostname, port)
    
    _connection = property(lambda self: User._get_shared_attr_with_default(self.__connstring, "connection", None))
    _password = property(lambda self: User._get_shared_attr_with_default(self.__connstring, "password", None))
    _password_callback = property(lambda self: User._get_shared_attr_with_default(self.__connstring, "password_callback", None))
    _password_group = property(lambda self: User._get_shared_attr_with_default(self.__connstring, "password_group", None))
    _expecter = property(lambda self: User._get_shared_attr_with_default(self.__connstring, "expecter", None))
    
    password = property(_get_password)
    username = property(lambda self: self._get_username_and_hostname_and_port()[0])
    hostname = property(lambda self: self._get_username_and_hostname_and_port()[1])
    port = property(lambda self: self._get_username_and_hostname_and_port()[2])

    def run(self, cmd, ignore_failure = False):
        """runs a command for this user, returning stdout and stderr
        as separate string objects, raising a ShellCommandFailedError if
        the command failed"""
        
        # check to see if we've already set our connection
        if not self._connection:
            
            # we need to create our connection
            connection = self._create_connection()
            User._set_shared_attr(self.__connstring, "connection", connection)
        
        # run the command
        try:
            
            # TODO: cleaner implementation
            command_stream = self._connection.create_command_stream(cmd)
            stdout_string = None
            stderr_string = None
            current_stdout_buffer = None
            current_stderr_buffer = None
            for stdout_line, stderr_line, stdout_byte, stderr_byte in command_stream:
                
                # track potential prompts in stdout/stderr
                if not current_stdout_buffer:
                    current_stdout_buffer = stdout_byte
                else:
                    current_stdout_buffer += stdout_byte
                if not current_stderr_buffer:
                    current_stderr_buffer = stderr_byte
                else:
                    current_stderr_buffer += stderr_byte
                
                # try responding to prompts seen in stdout/stderr
                if current_stdout_buffer:
                    if self._expecter.handle(current_stdout_buffer, command_stream.get_fake_stdin()) == True:
                        current_stdout_buffer = None
                if current_stderr_buffer:
                    if self._expecter.handle(current_stderr_buffer, command_stream.get_fake_stdin()) == True:
                        current_stderr_buffer = None
                    
                # track and collect lines of stdout
                if stdout_line:
                    if not stdout_string:
                        stdout_string = ""
                    stdout_string += "%s\n" % stdout_line
                
                # track and collect lines of stderr
                if stderr_line:
                    if not stderr_string:
                        stderr_string = ""
                    stderr_string += "%s\n" % stderr_line
            
        except NonzeroReturnStatusError, e:
            if ignore_failure:
                pass
            else:
                raise
            
        # return a dictionary pointing at a tuple --> {connstring: (stdout, stderr)}
        return _ShellCommandRunResponse({self: (stdout_string, stderr_string)})
    
    def configure(self, password_group = None, su_from = None, password = None, password_callback = None):
        """given a list of keyword arguments, converts those into configuration
        values for any user identified by this connection string"""
        
        # check to ensure that our connection has not been made
        if self._connection:

            # cannot reconfigure a user if we've already made our connection
            raise UserAlreadyConnectedError("user '%(connstring)s' is already connected, configuration changes not permitted" % dict(
                connstring = self.__connstring
            ))
        
        # set any attribute that was given
        if password_group:
            User._set_shared_attr(connstring = self.__connstring, key = "password_group", value = password_group)
            User._set_password_group(connstring = self.__connstring, groupname = password_group)
        if su_from:
            User._set_shared_attr(connstring = self.__connstring, key = "su_from", value = su_from)
        if password:
            User._set_shared_attr(connstring = self.__connstring, key = "password", value = password)
        if password_callback:
            User._set_shared_attr(connstring = self.__connstring, key = "password_callback", value = password_callback)
    
    def expect_input_for(self, pattern):
        """asks the user for input when `pattern` is recognized in the output stream"""
        self._expecter.expect_input_for(pattern)
    
    def expect_password_for(self, pattern):
        """asks the user for a password when `pattern` is recognized in the output stream.
        This is different from `expect_input_for` in that logging methods will
        echo the response with asterisks rather than plaintext (to protect
        the secrecy of the password in the logs)"""
        self._expecter.expect_password_for(pattern)
    
    def respond_with_input_for(self, pattern, plaintext):
        """responds with plaintext input when `pattern` is recognized in the output stream"""
        self._expecter.respond_with_input_for(pattern, plaintext)
    
    def respond_with_password_for(self, pattern, password):
        """responds with a password when `pattern` is recognized in the output stream.
        This is different from `respond_with_input_for` in that logging methods will
        echo the response with asterisks rather than plaintext (to protect
        the secrecy of the password in the logs)"""
        self._expecter.respond_with_password_for(pattern, password)
    
    def __init__(self, connstring, **kwargs):
        
        # set up logging
        self.__logger = logging.getLogger("cloudcontrol.users")
        
        # use our connection string as our unique identifier for this user
        self.__connstring = connstring
        
        # place ourselves in the default password group
        User._set_password_group(self.__connstring, None)
        
        # set up our expecter if needed
        if not self._expecter:
            User._set_shared_attr(connstring = self.__connstring, key = "expecter", value = Expecter())
        
        # also allow configuration with the remaining args
        self.configure(**kwargs)
    
    def __str__(self):
        return self.__connstring
    
    def __hash__(self):
        return hash(self.__connstring)
    
    def __eq__(self,other):
        if isinstance(other,User):
            return self._connection_string == other._connection_string
        return False
    
    def __ne__(self,other):
        return not self.__eq__(other)
    
    # def pull(self, source, destination):
    # 
    #     # get the source as a path object
    #     source_path = None
    #     if isinstance(source, path):
    #         source_path = source
    #     elif isinstance(source, basestring):
    #         source_path = path(source)
    #     
    #     # get the destination as a path object
    #     destination_path = None
    #     if isinstance(destination, path):
    #         destination_path = destination
    #     elif isinstance(destination, basestring):
    #         destination_path = path(source)
    #     
    #     # determine if the remote filename is actually a directory to place into
    #     path_separator = os.path.normcase('/')
    #     # if destination_path.endswith( path_separator ):
    #     #     destination_path = remote_filename + os.path.basename(local_filename)
    #     
    #     # get a queue of remote operations to perform
    #     remote_dirs_to_create = []
    #     files_to_send = {}
    #     
    #     if os.path.isdir( source_path ):
    #         
    #         def directory_visitor(arg, local_currdir, filenames):
    #             # parse the dir_mapping arg
    #             local_dir, remote_dir = arg
    #             
    #             # figure out the mapped directory
    #             remote_currdir = os.path.normpath( local_currdir.replace(local_dir, remote_dir) )
    #             remote_dirs_to_create.append( remote_currdir )
    #             
    #             for filename in filenames:
    #                 local_filename = os.path.join(local_currdir, filename)
    #                 if os.path.isfile(local_filename):
    #                     remote_filename = os.path.normpath(local_filename.replace(local_dir, remote_dir))
    #                     files_to_send[local_filename] = remote_filename
    #         
    #         # walk the directory structure
    #         os.path.walk(source_path, directory_visitor, (source_path, destination_path))
    #         
    #     else:
    #         
    #         files_to_send[local_filename] = remote_filename
    #     
    #     # sort the remote directory creation queue
    #     properly_ordered_remote_dirs_to_create = sorted(remote_dirs_to_create, lambda x,y: len(x) > len(y))
    #     
    #     # successfully pulled all send specifications, execute them all at once
    #     # TODO: parallelize this?
    #     sftp_client = self.ssh_client.open_sftp()
    #     
    #     for new_dir in properly_ordered_remote_dirs_to_create:
    #         self._mkdir(sftp_client, new_dir)
    #     
    #     for local_filename in files_to_send:
    #         remote_filename = files_to_send[local_filename]
    #         self._put(sftp_client, local_filename, remote_filename)

    # def expect_password_for(self, pattern):
    #     """adds a password expectation that the user must respond to"""
    #     expecter
    #     raise NotImplementedError("this is a placeholder during refactoring")

    # def expect_input_for(self, pattern):
    #     """adds an input expectation that the user must respond to"""
    #     raise NotImplementedError("this is a placeholder during refactoring")

    # def respond_with(self, pattern, response):
    #     """adds an expectation that is automatically responded to"""
    #     raise NotImplementedError("this is a placeholder during refactoring")
    
    # def respond_with_password(self, pattern, response):
    #     """adds a password expecation that is automatically responded to"""
    #     raise NotImplementedError("this is a placeholder during refactoring")

    # def stop_expecting(self, pattern):
    #     """tells the strategy to stop expecting input for a
    #     particular pattern (password or input)"""
    #     raise NotImplementedError("this is a placeholder during refactoring")

    # def stop_responding_to(self, pattern):
    #     """tells the strategy to stop automatically responding for
    #     a particular pattern (password or input)"""
    #     raise NotImplementedError("this is a placeholder during refactoring")
            
